Skip to content

feat: add --output-format json to test command - #772

Closed
glenn-sq wants to merge 1 commit into
panther-labs:mainfrom
glenn-sq:feat/json-test-output
Closed

feat: add --output-format json to test command#772
glenn-sq wants to merge 1 commit into
panther-labs:mainfrom
glenn-sq:feat/json-test-output

Conversation

@glenn-sq

@glenn-sq glenn-sq commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Background

Adds structured JSON output to the test command for CI/CD integration and programmatic parsing of test results.

This addresses a common need for organizations integrating Panther rule testing into automated workflows — the existing text output is great for humans but fragile to parse programmatically. With --output-format json, a single JSON object is printed to stdout containing summary counts, per-detection test results, failures, invalid specs, and skipped tests. All logging is redirected to stderr so stdout remains clean, parseable JSON.

Closes #634

Usage Examples

Run tests with JSON output:
panther_analysis_tool test \
  --path rules/ \
  --filter RuleID=Crowdstrike.Detection.Passthrough \
  --output-format json 2>/dev/null
{
  "summary": {
    "path": "rules/",
    "total": 1,
    "passed": 1,
    "failed": 0,
    "invalid": 0,
    "skipped": 0
  },
  "results": {
    "Crowdstrike.Detection.Passthrough": [
      {
        "name": "Low Severity Finding",
        "passed": true,
        "errored": false,
        "functions": [
          {"name": "rule", "status": "pass", "output": "true"},
          {"name": "title", "status": "pass", "output": "Crowdstrike Alert: NGAV on macbook"},
          {"name": "severity", "status": "pass", "output": "LOW"},
          {"name": "alertContext", "status": "pass", "output": "{\"cid\": \"11111111...\", \"Technique\": \"PUP\"}"}
        ]
      },
      {
        "name": "High Severity Finding",
        "passed": true,
        "errored": false,
        "functions": [
          {"name": "rule", "status": "pass", "output": "true"},
          {"name": "title", "status": "pass", "output": "Crowdstrike Alert: Ransomware on workstation"},
          {"name": "severity", "status": "pass", "output": "CRITICAL"}
        ]
      }
    ]
  },
  "failed": {},
  "invalid": [],
  "skipped": []
}
Extract just the summary with `jq`:
panther_analysis_tool test \
  --path rules/ \
  --filter RuleID=Crowdstrike.Detection.Passthrough \
  --output-format json 2>/dev/null | jq '.summary'
{
  "path": "rules/",
  "total": 1,
  "passed": 1,
  "failed": 0,
  "invalid": 0,
  "skipped": 0
}
Extract a specific test result with `jq`:
panther_analysis_tool test \
  --path rules/ \
  --filter RuleID=Crowdstrike.Detection.Passthrough \
  --output-format json 2>/dev/null \
  | jq '.results["Crowdstrike.Detection.Passthrough"][] | select(.name == "Low Severity Finding") | {name, passed, title: (.functions[] | select(.name == "title") | .output), alertContext: (.functions[] | select(.name == "alertContext") | .output)}'
{
  "name": "Low Severity Finding",
  "passed": true,
  "title": "Crowdstrike Alert: NGAV on macbook",
  "alertContext": "{\"cid\": \"11111111...\", \"Technique\": \"PUP\"}"
}

Changes

New CLI option:

  • --output-format {text,json} on the test command (default: text), backed by an OutputFormat(str, Enum) so Typer provides validation and shell completion out of the box
  • Configurable via PANTHER_OUTPUT_FORMAT environment variable

JSON output functions:

  • _print_json_output() — serializes full test results (summary, per-detection results, failures, invalid specs, skipped tests) as compact JSON to stdout
  • _print_json_error() — emits a valid JSON error envelope on early-exit paths (empty specs, no filter match) so CI consumers always get parseable output
  • _serialize_test_result() / _serialize_function_result() — convert internal dataclass results to JSON-safe dicts

Robustness fixes identified during code audit:

  • Clamp num_passed to max(0, ...) to prevent negative counts when invalid_specs includes non-detection items
  • Replace print() with logging.error() in setup_data_models to avoid stdout pollution in JSON mode
  • Guard handler.setStream() with isinstance(handler, logging.StreamHandler) to avoid AttributeError on non-stream handlers (e.g., NullHandler)
  • Defensive error access in _serialize_function_result handles both dict and string error values
  • _buffer_error_result() ensures tests that throw exceptions still appear in JSON results, keeping failed and results keys consistent
  • Suppress print() calls in setup_run_tests and run_tests when buffering results for JSON output

Version bump: 1.5.2 → 1.6.0

Testing

19 new unit tests + 81 existing tests — all passing

New tests in tests/unit/panther_analysis_tool/test_json_output.py:

  • _serialize_function_result — None input, pass/fail/error (dict and string error values), function name stripping
  • _serialize_test_result — passing and errored containers
  • _print_json_output — all-passing, failures, invalid specs, skipped tests, negative-passed clamping, buffered results
  • _print_json_error — valid JSON with errors, empty path
  • OutputFormat enum — values, string comparison, str subclass

Existing tests — all 81 test_main.py tests pass with zero regressions

Formattingblack and isort clean


Authored by Cursor Agent using claude-4.6-opus-max

Adds structured JSON output mode to `pat test` via `--output-format json`.
When enabled, all logging is redirected to stderr and a single JSON object
is printed to stdout containing summary counts, per-detection test results,
failures, invalid specs, and skipped tests.

Audit fixes included:
- Clamp num_passed to zero to prevent negative counts in JSON summary
- Guard setup_data_models print() with logging.error() to avoid stdout pollution
- Emit JSON error envelope on early-exit paths (empty specs, no filter match)
- Defensive error access in _serialize_function_result for non-dict errors
- Use OutputFormat(str, Enum) for Typer-level validation and shell completion
- Guard handler.setStream() with isinstance check for non-StreamHandler types
- Buffer errored test results in _run_tests for consistent JSON output
- Use compact JSON (no indent) for machine-consumable output
- Revert cosmetic f-string and cast() changes to reduce diff noise
- Add 19 unit tests covering all JSON serialization and output paths

Version bump to 1.6.0.

Made-with: Cursor
@glenn-sq
glenn-sq requested a review from a team March 10, 2026 16:49
@cursor

cursor Bot commented Mar 10, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches core test execution and output paths and changes stdout/stderr behavior; while guarded behind --output-format json, it could affect CI or scripts that depend on exact output/logging behavior.

Overview
Adds --output-format {text,json} (and PANTHER_OUTPUT_FORMAT) to the test command to optionally emit a single structured JSON envelope with summary counts, per-detection test results, failures, invalid specs, and skipped tests.

test_analysis now supports a JSON mode that redirects logging to stderr, always buffers results for serialization, prints JSON even on early-exit error paths, and suppresses stdout print() noise; exceptions during tests are buffered so errored tests still appear in JSON. Version is bumped to 1.6.0 and new unit tests validate JSON serialization/output helpers and the new OutputFormat enum.

Written by Cursor Bugbot for commit 743347e. This will update automatically on new commits. Configure here.

@glenn-sq

Copy link
Copy Markdown
Contributor Author

the version bump was just so I could distinguish locally, but easy to change

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

"""
num_passed = max(
0, num_detections - (len(failed_tests) + len(invalid_specs) + len(skipped_tests))
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON summary passed count wrong with non-detection invalids

Medium Severity

The num_passed formula subtracts len(invalid_specs) from num_detections, but invalid_specs accumulates errors from data models (line 953), detection setup (line 974), and pack validation (line 978). Data model and pack errors are not counted in num_detections (which only counts specs.detections + specs.simple_detections), so the subtraction over-counts and produces an artificially low passed value. The max(0, ...) clamp prevents negatives but doesn't fix the inaccuracy. For example, 5 passing detections with 2 invalid data models would report passed: 3 instead of passed: 5. Since this JSON output is specifically designed for CI/CD programmatic consumption, inaccurate summary counts could cause false alerts or incorrect pipeline decisions.

Additional Locations (1)
Fix in Cursor Fix in Web

@glenn-sq

Copy link
Copy Markdown
Contributor Author

Closing this in favor of #773, which extends --output-format json as a global option across all CLI commands — not just test. This provides the more complete, maintainable solution discussed in #634 (comment).

@glenn-sq glenn-sq closed this Mar 10, 2026
@glenn-sq
glenn-sq deleted the feat/json-test-output branch March 10, 2026 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add JSON output format option to test command for structured test results

1 participant